package com.ybox.gateway.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.ybox.common.core.constant.FilterOrderConstant;
import com.ybox.common.core.constant.SecurityConstants;
import com.ybox.common.core.utils.ServletUtils;
import com.ybox.common.core.utils.StringUtils;
import com.ybox.common.core.utils.sign.SignUtil;
import com.ybox.common.redis.service.RedisService;
import com.ybox.gateway.config.properties.IgnoreSignWhiteProperties;
import com.ybox.gateway.context.GatewayContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;

/**
 * 签名校验
 *
 * @author ych
 */
@Component
public class ApiVerifyFilter implements GlobalFilter, Ordered {

    private static final Logger log = LoggerFactory.getLogger(ApiVerifyFilter.class);

    // 排除过滤的 uri 地址，nacos自行添加
    @Autowired
    private IgnoreSignWhiteProperties ignoreWhite;

    @Autowired
    private RedisService redisService;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String url = request.getURI().getPath();

        // 未开启动api签名不验证
        if (!ignoreWhite.getEnable()){
            return chain.filter(exchange);
        }
        // 跳过不需要验证的路径
        if (StringUtils.matches(url, ignoreWhite.getWhites())) {
            return chain.filter(exchange);
        }
        // 内部访问不验证
        if (exchange.getRequest().getHeaders().get(SecurityConstants.FROM_SOURCE) != null && exchange.getRequest().getHeaders().get(SecurityConstants.FROM_SOURCE).get(0) == SecurityConstants.INNER) {
            return chain.filter(exchange);
        }

        if (request.getMethod() == HttpMethod.GET) {
            // get请求 校验参数
            return verifyGetMethod(exchange, chain, request);
        }

        if (request.getMethod() == HttpMethod.POST) {
            // post请求 校验参数
            return verifyPostMethod(exchange, chain, request);
        }

        return chain.filter(exchange);
    }

    /**
     * get请求 校验参数
     *
     * @param exchange
     * @param chain
     * @param request
     * @return
     */
    private Mono<Void> verifyGetMethod(ServerWebExchange exchange, GatewayFilterChain chain, ServerHttpRequest request) {
        // get请求获取参数
        Map<String, String> queryParamMap = request.getQueryParams().toSingleValueMap();

        // 校验参数逻辑
        return verifySign(exchange, chain, queryParamMap);
    }

    /**
     * post请求 校验参数
     *
     * @param exchange
     * @param chain
     * @param request
     * @return
     */
    private Mono<Void> verifyPostMethod(ServerWebExchange exchange, GatewayFilterChain chain, ServerHttpRequest request) {
        try {
            GatewayContext gatewayContext = (GatewayContext) exchange.getAttributes().get(GatewayContext.CACHE_GATEWAY_CONTEXT);
            // get body from gatewayContext
            String cacheBody = gatewayContext.getCacheBody();

            Map map = new ObjectMapper().readValue(cacheBody, Map.class);

            // 校验参数逻辑
            return verifySign(exchange, chain, map);

        } catch (Exception e) {
            log.error("解析body内容失败：{}", e);
            // 403
            return signErrorResponse(exchange, "Forbidden");
        }
    }

    /**
     * 校验参数
     *
     * @param exchange
     * @param chain
     * @param map
     * @return
     */
    private Mono<Void> verifySign(ServerWebExchange exchange, GatewayFilterChain chain, Map map) {
        // 获取请求头
        TreeMap<String, String> headers = getHeaders(exchange);

        if (StringUtils.isEmpty(headers.get(SignUtil.SIGN))) {
            return signErrorResponse(exchange, "签名不能为空");
        } else {
            if (StringUtils.isBlank(headers.get(SignUtil.X_CA_NONCE))) {
                return signErrorResponse(exchange, "随机字符串不能为空");
            }

            if (StringUtils.isBlank(headers.get(SignUtil.X_CA_TIMESTAMP))) {
                return signErrorResponse(exchange, "时间戳不能为空");
            }

            long timestamp = 0;
            try {
                timestamp = Long.parseLong(headers.get(SignUtil.X_CA_TIMESTAMP));
            } catch (Exception e) {
                log.error("发生异常", e);
            }
            // 请求传过来的时间戳与服务器当前时间戳差值大于5000，则当前请求的timestamp无效
            if (Math.abs(timestamp - System.currentTimeMillis()) > 500000) {
                return signErrorResponse(exchange, "签名已过期");
            }

            // 请求传过来的随机数如果在redis中存在，则当前请求的nonce无效
            boolean nonceExists = redisService.hasKey(getSignKey(headers.get(SignUtil.X_CA_TIMESTAMP), headers.get(SignUtil.X_CA_NONCE)));
            if (nonceExists) {
                return signErrorResponse(exchange, "随机字符串已存在");
            }

            // 根据请求传过来的参数构造签名，如果和接口的签名不一致，则请求参数被篡改
            TreeMap<String, Object> signTreeMap = new TreeMap<>();
            signTreeMap.putAll(map);
            String currentSign = SignUtil.signature(exchange.getRequest().getMethodValue().toLowerCase(), String.valueOf(exchange.getRequest().getPath()), headers, signTreeMap);
            if (!headers.get(SignUtil.SIGN).equalsIgnoreCase(currentSign)) {
                return signErrorResponse(exchange, "签名不匹配");
            }

            // 存入redis
            redisService.setCacheObject(getSignKey(headers.get(SignUtil.X_CA_TIMESTAMP), headers.get(SignUtil.X_CA_NONCE)), headers.get(SignUtil.SIGN), 120L, TimeUnit.MILLISECONDS);
            // 校验成功，则当前过滤器执行完毕
            return chain.filter(exchange);
        }
    }

    private Mono<Void> signErrorResponse(ServerWebExchange exchange, String msg) {
        log.error("[签名异常处理]请求路径:{} 原因:{}", exchange.getRequest().getPath(), msg);
        return ServletUtils.webFluxResponseWriter(exchange.getResponse(), "签名异常", com.ybox.common.core.constant.HttpStatus.SIGN_ERROR);
    }

    /**
     * 获取缓存key
     */
    private String getSignKey(String timeStamp, String nonce) {
        return SignUtil.SIGN_KEY + timeStamp + nonce;
    }

    /**
     * 获取验证所需请求头
     *
     * @param exchange
     * @return
     */
    private TreeMap<String, String> getHeaders(ServerWebExchange exchange) {
        HttpHeaders httpHeaders = exchange.getRequest().getHeaders();
        List<String> contentType = httpHeaders.get(HttpHeaders.CONTENT_TYPE);
        List<String> contentMd5 = httpHeaders.get(SignUtil.CONTENT_MD5);
        List<String> caDate = httpHeaders.get(SignUtil.CA_DATE);
        List<String> xCaKey = httpHeaders.get(SignUtil.X_CA_KEY);
        List<String> xCaTimestamp = httpHeaders.get(SignUtil.X_CA_TIMESTAMP);
        List<String> xCaNonce = httpHeaders.get(SignUtil.X_CA_NONCE);
        List<String> sign = httpHeaders.get(SignUtil.SIGN);
        TreeMap<String, String> headers = new TreeMap<>();
        if (!CollectionUtils.isEmpty(contentType) && (contentType.contains(MediaType.APPLICATION_JSON_VALUE) || contentType.contains(MediaType.APPLICATION_JSON_UTF8_VALUE))) {
            headers.put(HttpHeaders.CONTENT_TYPE, contentType.get(0));
        }
        // md5是从请求体中拿
        if (!CollectionUtils.isEmpty(contentMd5)) {
            headers.put(SignUtil.CONTENT_MD5, contentMd5.get(0));
        }
        if (!CollectionUtils.isEmpty(caDate)) {
            headers.put(SignUtil.CA_DATE, caDate.get(0));
        }
        if (!CollectionUtils.isEmpty(xCaKey)) {
            headers.put(SignUtil.X_CA_KEY, xCaKey.get(0));
        }
        if (!CollectionUtils.isEmpty(xCaTimestamp)) {
            headers.put(SignUtil.X_CA_TIMESTAMP, xCaTimestamp.get(0));
        }
        if (!CollectionUtils.isEmpty(xCaNonce)) {
            headers.put(SignUtil.X_CA_NONCE, xCaNonce.get(0));
        }
        if (!CollectionUtils.isEmpty(sign)) {
            headers.put(SignUtil.SIGN, sign.get(0));
        }
        headers.put("path", exchange.getRequest().getURI().getPath());
        return headers;
    }


    @Override
    public int getOrder() {
        return FilterOrderConstant.getOrder(this.getClass().getName());
    }
}

